package org.nobody.mallb2c.mallproduct.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import org.nobody.mallb2c.mallproduct.service.CategoryBrandRelationService;
import org.nobody.mallb2c.mallproduct.vo.Catelog2Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.nobody.common.utils.PageUtils;
import org.nobody.common.utils.Query;

import org.nobody.mallb2c.mallproduct.dao.CategoryDao;
import org.nobody.mallb2c.mallproduct.entity.CategoryEntity;
import org.nobody.mallb2c.mallproduct.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;


@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
  /**
   * ↓ 不用再手动注入 因为baseMapper其实就是这里ServiceImpl<CategoryDao, CategoryEntity> 的 CategoryDao
   */
  //@Autowired
  //CategoryDao categoryDao;

  @Autowired
  CategoryBrandRelationService categoryBrandRelationService;

  @Autowired
  private StringRedisTemplate redisTemplate;

  @Autowired
  RedissonClient redissonClient;

  @Override
  public PageUtils queryPage(Map<String, Object> params) {
    IPage<CategoryEntity> page = this.page(
      new Query<CategoryEntity>().getPage(params),
      new QueryWrapper<CategoryEntity>()
    );

    return new PageUtils(page);
  }

  @Override
  public List<CategoryEntity> listWithTree() {
    //1、 查出所有分类
    List<CategoryEntity> entities = baseMapper.selectList(null);

    //2、 组装成父子的树形结构
    //2.1)、 找到所有的一级分类
    List<CategoryEntity> level1Menus = entities.stream().
      filter(categoryEntity -> categoryEntity.getParentCid().equals(0L)).
      map(menu -> {
        menu.setChildren(getChildren(menu, entities));
        return menu;
      }).
      /** ↓即sort的数值越小越排在前面*/
        sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())).
        collect(Collectors.toList());

    return level1Menus;
  }

  private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
    List<CategoryEntity> children = all.stream().
      filter(categoryEntity -> categoryEntity.getParentCid().equals(root.getCatId())).
      map(categoryEntity -> {
        categoryEntity.setChildren(getChildren(categoryEntity, all));
        return categoryEntity;
      }).
      sorted((menu1, menu2) -> (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())).
      collect(Collectors.toList());

    return children;
  }

  @Override
  public void removeMenuByIds(List<Long> asList) {
    //TODO 1、检查当前删除的菜单,是否被别的地方引用

    //2、逻辑删除
    baseMapper.deleteBatchIds(asList);
  }

  /**
   * 找到cateLogId的完整路径
   * [父/子/孙]
   *
   * @param catelogId
   * @return
   */
  @Override
  public Long[] findCatelogPath(Long catelogId) {
    List<Long> paths = new ArrayList<>();
    List<Long> parentPath = findParentPath(catelogId, paths);

    Collections.reverse(parentPath);

    return parentPath.toArray(new Long[parentPath.size()]);
  }

  /**
   * 级联更新所有关联的数据
   *
   * @param category
   */
  //@CacheEvict: 清除缓存
  //@CacheEvict(value = "category",key = "'getLevel1Categorys'"/*←因为key接受的是一个spEL表达式,故如果直接设置普通字符串,这里要使用''包裹*/) //缓存一致性 の 试下模式

  //=== 一次清除多个缓存 ===
  //法一: 使用@Caching组合操作
  /*@Caching(evict = {
    @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    @CacheEvict(value = "category",key = "'getCatalogJson'")
  })*/
  //法二: 删除category分区里的所有数据
  /*一般来说 同一类型的数据 都放在同一个缓存分区里(故最好缓存前缀就使用默认的分区名 以便区分)
  */
  @CacheEvict(value = "category",allEntries = true)
  @Transactional //←需要在MyBatisConfig配置类中使用@EnableTransactionManagement开启事务
  @Override
  public void updateCascade(CategoryEntity category) {
    this.updateById(category);
    categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
  }

  //@Cacheable代表当前方法的结果需要缓存
  //+ 如果缓存中有,方法不用调用。
  //+ 如果缓存中没有,会调用方法,并将方法的结果放入缓存。
  //每一个需要缓存的数据我们都需要指定一个名字(最好按照业务类型分)
  /*
    默认行为
    1)、 如果缓存命中,方法不用调用
    2)、 key默认自动生成,格式为: 缓存的名字::SimpleKey []
    3)、 缓存的value的值。 默认使用jdk序列化机制。 将序列化后的数据存到redis
    4)、 默认ttl时间 -1; 即永久

    自定义:
    1)、 指定生成的缓存使用的key  key属性指定,接受一个SpEL(https://docs.spring.io/spring-framework/docs/5.1.17.RELEASE/spring-framework-reference/integration.html#cache-spel-context)
    2)、 指定缓存的数据的存活时间 配置文件中修改ttl
    3)、 将数据保存为json格式: 自定义RedisCacheConfiguration即可

  * */
  @Cacheable(
    //注意和缓存的前缀区别,这里设置的是缓存的名字.但是缓存前缀没有手动设置时,会以这里的名字作为缓存的key的前缀
    //缓存的名字 只是spring cache里的分区 而redis数据库里本身并没有什么分区
    value = {"category"/*,"category2"*/}
    ,key = "#root.method.name"
    ,sync = true
  ) //"category::getLevel1Categorys"
  @Override
  public List<CategoryEntity> getLevel1Categorys() {
    System.out.println("getLevel1Categorys");
    List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    return categoryEntities;
  }

  //TODO 如果对此进行鸭梨测试 会报: OutOfDirectMemoryError (direct memory 直接内存 即堆外内存 直接操作内存条的)
  //springboot2.0以后默认使用lettuce作为操作redis的客户端
  //它使用netty进行网络通讯
  //但是有bug lettuce底层操作netty时会导致堆外内存溢出 (资源没有即使释放所导致)
  //netty如果没有指定堆外内存,会默认使用我们指定的 -Xmx300m (VM options)
  //但我们其实应该通过设置 Dio.netty.maxDirectMemory 来调大堆外内存
  //但仅仅是调大堆外内存 只会延缓错误的发生
  //
  //解决方案:
  //法一)、 升级lettuce客户端
  //法二)、 切换使用jedis (比较老,没更新了,lettuce性能更高(使用了netty))

  /*@Override
  public Map<String, List<Catelog2Vo>> getCatalogJson() {
    String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"*//*←JSON字符串*//*);

    if (StringUtils.isEmpty(catalogJSON)) { //缓存中没有
      System.out.println("缓存没命中...将要查询数据库...");
      //从数据库中查找数据
      Map<String, List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDbWithRedissonLock();

      return catalogJsonFromDb;
    }

    System.out.println("缓存命中...直接返回...");
    Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
    });

    return result;
  }*/

  //
  //使用SpringCache帮我们实现的缓存机制
  @Cacheable(value = "category",key = "#root.methodName")
  @Override
  public Map<String, List<Catelog2Vo>> getCatalogJson() {
    System.out.println("查询了数据库...");

    List<CategoryEntity> allCatEntities = baseMapper.selectList(null);

    //1、 查出所有1级分类
    List<CategoryEntity> level1Categorys = getCateListByParentCid(allCatEntities, 0L);

    //2、 封装数据
    Map<String, List<Catelog2Vo>> catalogJson = level1Categorys.stream().collect(Collectors.toMap(
      k -> k.getCatId().toString(),
      v -> {
        // 每一个的一级分类,查到这个一级分类的二级分类
        List<CategoryEntity> categoryEntities = getCateListByParentCid(allCatEntities, v.getCatId());

        // 封装
        List<Catelog2Vo> catelog2Vos = null;
        if (categoryEntities != null) {
          catelog2Vos = categoryEntities.stream().map(l2 -> {
            Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getName(), l2.getCatId().toString());

            //找当前二级分类的三级分类封装成vo
            List<CategoryEntity> level3Catelog = getCateListByParentCid(allCatEntities, l2.getCatId());
            if (level3Catelog != null) {
              List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                return catelog3Vo;
              }).collect(Collectors.toList());
              catelog2Vo.setCatalog3List(collect);
            }

            return catelog2Vo;
          }).collect(Collectors.toList());
        }
        return catelog2Vos;
      }
    ));

    return catalogJson;
  }


  /**
   * 使用手写redis分布式锁 解决缓存击穿
   *
   * @return
   */
  @Override
  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithDCSLock() {
    //1、 占分布式锁
    String uuid = UUID.randomUUID().toString();
    Boolean lock = redisTemplate.opsForValue().setIfAbsent/*←就是redis客户端的setnx*/("lock", uuid/*←只*/,/*2、 设置过期时间 防止死锁(仅仅靠try是不够的,因为可能服务会突然宕掉,这样连错误都来不及抛)→*/3, TimeUnit.SECONDS/*←设置大点 以免有些业务过长,需要续期*/);

    if (lock) {
      System.out.println("获取分布式锁成功");
      Map<String, List<Catelog2Vo>> catalogJsonFromDb = null;
      try {
        //业务代码
        catalogJsonFromDb = getCatalogJsonFromDb();

      } finally { //仅仅靠try是不够的 还要设置过期时间 防止死锁, 因为可能服务会突然宕掉,这样连错误都来不及抛

        //但这样其实还有问题 因为和redis交互消耗的,故我们虽然要删除前的那次核查查询出来的key是一样的,但又由于在这次查询到我们发送请求到redis,到redis接收到准备删除,这一时间段内,key也可能是会过期的,会被抢占的,so也是会变的
/*
String lockValue = redisTemplate.opsForValue().get("uuid");
if (uuid.equals(lockValue)) {
  redisTemplate.delete("lock"); //释放锁
}
* */

        //So 最终选择使用lua脚本进行原子操作 将要删除前的查询和删除两个操作绑定到一起 不可分割
        String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
        //↑ 成功返回1 失败返回0

        //删除锁
        redisTemplate.execute(new DefaultRedisScript<Long/*←脚本返回的类型*/>(script, Long.class/*←脚本返回的类型*/), Arrays.asList("lock"), uuid);

        return catalogJsonFromDb;
      }

    } else {
      //加锁失败...重试
      System.out.println("获取分布式锁成功...等待重试");
      try {
        Thread.sleep(50);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      return getCatalogJsonFromDbWithDCSLock(); //自旋的方式
    }
  }

  /**
   * 使用成熟的redisson分布式锁 解决缓存击穿
   *
   * @return
   */
  @Override
  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock() {
    /*
      锁的命名 以锁的粒度来命名
      product-11-lock
      product-12-lock
    * */
    RLock lock = redissonClient.getLock("catalogJson-lock");
    lock.lock();
    System.out.println("获取分布式锁成功");

    Map<String, List<Catelog2Vo>> catalogJsonFromDb = null;
    try {
      //业务代码
      catalogJsonFromDb = getCatalogJsonFromDb();

    } finally {
      lock.unlock();
    }
    return catalogJsonFromDb;
  }

  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDb() {
    //得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
    //因为同时进到 本方法 的线程(请求)可能有很多,进来之前都认为redis没有数据,但只要最先抢到锁的那个查询到数据存入redis后,其实redis就已经有数据了,后面没有第一时间抢到锁的但已经进到该方法的请求(线程)其实没有必要再从mysql数据库中去查找了而应该直接从redis缓存中获取
    //故这里再次对redis进行了查询
    String catalogJSON = redisTemplate.opsForValue().get("catalogJSON"/*←JSON字符串*/);

    if (!StringUtils.isEmpty(catalogJSON)) { //缓存不为空则直接返回
      Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Catelog2Vo>>>() {
      });

      return result;
    }
    System.out.println("查询了数据库...");

    List<CategoryEntity> allCatEntities = baseMapper.selectList(null);

    //1、 查出所有1级分类
    List<CategoryEntity> level1Categorys = getCateListByParentCid(allCatEntities, 0L);

    //2、 封装数据
    Map<String, List<Catelog2Vo>> catalogJson = level1Categorys.stream().collect(Collectors.toMap(
      k -> k.getCatId().toString(),
      v -> {
        // 每一个的一级分类,查到这个一级分类的二级分类
        List<CategoryEntity> categoryEntities = getCateListByParentCid(allCatEntities, v.getCatId());

        // 封装
        List<Catelog2Vo> catelog2Vos = null;
        if (categoryEntities != null) {
          catelog2Vos = categoryEntities.stream().map(l2 -> {
            Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getName(), l2.getCatId().toString());

            //找当前二级分类的三级分类封装成vo
            List<CategoryEntity> level3Catelog = getCateListByParentCid(allCatEntities, l2.getCatId());
            if (level3Catelog != null) {
              List<Catelog2Vo.Catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                return catelog3Vo;
              }).collect(Collectors.toList());
              catelog2Vo.setCatalog3List(collect);
            }

            return catelog2Vo;
          }).collect(Collectors.toList());
        }
        return catelog2Vos;
      }
    ));

    //放入缓存
    redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(catalogJson), 1, TimeUnit.DAYS);

    return catalogJson;
  }

  /**
   * 采用本地锁 解决缓存击穿
   * <p>
   * + synchronized(this): 由于SpringBoot中所有组件在容器中都是单例的,故即使是百万请求,这里的this也是同一个,也就是同一把锁(如果该微服务没有多台,但这里即使是做了高可用,有多台,也不存在,因为即使是100台商品微服务服务器,也才同时放100个sql去数据库查询)
   * <p>
   * + JUC(lock)
   *
   * @return
   */
  @Override
  public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithLocalLock() {

    synchronized (this) {

      return getCatalogJsonFromDb();
    }

  }

  private List<CategoryEntity> getCateListByParentCid(List<CategoryEntity> allCatEntities, Long parent_cid) {
    return allCatEntities.stream().filter(item -> item.getParentCid() == parent_cid).collect(Collectors.toList());
  }

  private List<Long> findParentPath(Long catelogId, List<Long> paths) {
    paths.add(catelogId);
    CategoryEntity byId = this.getById(catelogId);
    if (byId.getParentCid() != 0) {
      findParentPath(byId.getParentCid(), paths);
    }
    return paths;
  }
}
